﻿using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using MadLibrary.Core.Logging;
using MadLibrary.Net.Pop3.Exceptions;

namespace MadLibrary.Net.Pop3
{
    /// <summary>
    /// Pop3 TCP Client
    /// </summary>
    public class Pop3TcpClient : TcpClient
    {
        /// <summary>
        /// Pop3 default server port
        /// </summary>
        protected const int Pop3DefaultServerPort = 110;

        /// <summary>
        /// Pop3 SSL server port
        /// </summary>
        protected const int Pop3SslServerPort = 995;

        /// <summary>
        /// Connection timeout
        /// </summary>
        public int ConnectTimeout { get; set; }

        /// <summary>
        /// Use SSL
        /// </summary>
        public bool UseSsl { get; set; }

        /// <summary>
        /// Log
        /// </summary>
        protected ILog Log { get; set; }

        private Stream _dataStream;

        internal Pop3TcpClient(bool useSsl)
        {
            Log = new TraceLogger();

            ConnectTimeout = 5000;
            UseSsl = useSsl;
        }

        internal void Connect(string serverAddress)
        {
            try
            {
                var task =
                    Task.Factory.StartNew(
                        () =>
                        {
                            IPAddress serverIpAddress;

                            var pop3Port = UseSsl ? Pop3SslServerPort : Pop3DefaultServerPort;

                            if (IPAddress.TryParse(serverAddress, out serverIpAddress))
                            {
                                if(UseSsl)
                                    throw new Pop3Exception("Can't use IP address while with SSL connection. Please use DNS host name.");

                                Connect(new IPEndPoint(serverIpAddress, pop3Port));
                            }
                            else
                            {
                                Connect(serverAddress, pop3Port);
                            }
                        });

                if (!task.Wait(ConnectTimeout))
                    throw new TimeoutException("Connection to " + serverAddress + ":110 was timed out.");

                if (!UseSsl)
                {
                    _dataStream = GetStream();
                }
                else
                {
                    _dataStream =
                        new SslStream(
                            GetStream(), false, CertificateValidationCallback, CertificateSelectionCallback);

                    ((SslStream)_dataStream).AuthenticateAsClient(serverAddress);
                }
            }
            catch (SocketException e)
            {
                if (e.SocketErrorCode == SocketError.TimedOut)
                    throw new TimeoutException(
                        "Connection to " + serverAddress + ":110 was timed out.", e);
            }
        }

        internal string ReadTextUntilEnd(string endOfDataSign)
        {
            if (!_dataStream.CanRead)
                throw new Pop3Exception("Stream is not readable.");

            var readBuffer = new byte[1024];
            var completeMessage = new StringBuilder();

            do
            {
                var numOfReadBytes = _dataStream.Read(readBuffer, 0, readBuffer.Length);

                var readData = Encoding.ASCII.GetString(readBuffer, 0, numOfReadBytes);

                completeMessage.Append(readData);
            } while (!completeMessage.ToString().EndsWith(endOfDataSign));

            var message = completeMessage.ToString();

            Log.Debug("POP3 RECEIVED: " + message);

            return message;
        }

        internal void WriteTextLine(string data)
        {
            WriteText(data + "\r\n");
        }

        internal void WriteText(string data)
        {
            if (!_dataStream.CanWrite)
                throw new Pop3Exception("Stream is not writeable.");

            var writeBuffer = Encoding.ASCII.GetBytes(data);

            _dataStream.Write(writeBuffer, 0, writeBuffer.Length);

            Log.Debug("POP3 SEND: " + data);
        }

        private X509Certificate CertificateSelectionCallback(
            object sender, string targetHost, X509CertificateCollection localCertificates,
            X509Certificate remoteCertificate, string[] acceptableIssuers)
        {
            return remoteCertificate;
        }

        private bool CertificateValidationCallback(
            object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return sslPolicyErrors == SslPolicyErrors.None;
        }
    }
}
